/*
 * Copyright 2009 André Luiz Alves Moraes
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * */

package org.alamoraes.codegen;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import org.alamoraes.codegen.fileservices.FileBackupService;
import org.alamoraes.codegen.impl.SystemPropertiesImpl;
import org.alamoraes.codegen.readerswriters.Utf8ReaderWriterBuilder;
import org.alamoraes.codegen.template.MethodExtenderImpl;
import org.alamoraes.sfactory.IObjectConstructor;
import org.alamoraes.sfactory.ObjectFactory;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.util.introspection.Info;

/**
 *
 * @author andre
 */
public class CodeBuilder extends CodeFile {
    private File targetFile;
    private File templateFile;
    private Map<String,Object> codeModel;
    private IStreamReaderWriterBuilder srwBuilder;

    public CodeBuilder() {
    }

    /**
     * Constructor
     * @param targetFile File to be generated
     * @param templateFile Template
     * @param codeModel Code Model
     */
    public CodeBuilder(File targetFile, File templateFile, Map<String, Object> codeModel) {
        this.targetFile = targetFile;
        this.templateFile = templateFile;
        this.codeModel = codeModel;
    }

    public Map<String, Object> getCodeModel() {
        return codeModel;
    }

    public void setCodeModel(Hashtable<String, Object> codeModel) {
        this.codeModel = codeModel;
    }

    public File getTargetFile() {
        return targetFile;
    }

    public void setTargetFile(File targetFile) {
        this.targetFile = targetFile;
    }

    public File getTemplateFile() {
        return templateFile;
    }

    public void setTemplateFile(File templateFile) {
        this.templateFile = templateFile;
    }

    /**
     * Return an instance of the Stream Reader Writer builder
     * @return
     */
    protected IStreamReaderWriterBuilder getReaderWriterBuilder() {
        if (srwBuilder == null) {
            srwBuilder = AmCodeGenConsts.getObjectFactory().getInstance(IStreamReaderWriterBuilder.class);
        }
        return srwBuilder;
    }

    /**
     * Generate the target file with the template.
     * If any user blocks are present in the old file, the old ones are used
     * @throws IOException
     * @throws ResourceNotFoundException
     * @throws ParseErrorException
     * @throws Exception
     */
    public void generate() throws IOException, Exception {
        checkValidTargetFile();
        makeBackupOfOriginal();

        CodeSafetyMarksLoader safeMarks = new CodeSafetyMarksLoader();
        if (getTargetFile().isFile())
            safeMarks.parseFile(targetFile);

        VelocityEngine ve = configureVelocity();
        VelocityContext vc = new VelocityContext(getCodeModel());
        configureExtendedMethods(vc);
        Template t = null;
        vc.put("amcgg", new Hashtable());
        
        t = ve.getTemplate(templateFile.getAbsolutePath());

        BufferedWriter bw = new BufferedWriter(getReaderWriterBuilder().getWriter(new FileOutputStream(targetFile)));
        t.merge(vc, bw);

        bw.flush();
        bw.close();

        replaceUserBlocks(safeMarks);
    }

    /**
     * Configure the extended methods
     * @param vc
     */
    protected void configureExtendedMethods(VelocityContext vc) {
        EventCartridge events = new EventCartridge();
        IMethodExtender ime = AmCodeGenConsts.getObjectFactory().getInstance(IMethodExtender.class);
        registerExtensions(ime);
        ime.extendCartrige(events);
        vc.attachEventCartridge(events);
    }

    /**
     * Register the extensions on the method extender instance.
     * This instance is later used with the EventCartrige of the VelocityContext
     * @param ime
     */
    protected void registerExtensions(IMethodExtender ime) {
        extendMethodUpFirst(ime);
    }

    /**
     * Extend the template adding the method upFirst, wich change the first letter of the string to UpCase
     * @param ime
     */
    protected void extendMethodUpFirst(IMethodExtender ime) {
        ime.registerGet(new IGetMethodExtension() {

            public String getPropertyName() {
                return "upFirst";
            }

            public Object extend(Context context, String reference, Object object, Info info) {
                char[] vet = object.toString().toCharArray();
                if (vet.length > 0) {
                    vet[0] = Character.toUpperCase(vet[0]);
                }
                return new String(vet);
            }
        });
    }

    public void printGenerated(PrintStream ps) {
        try {
            BufferedReader br = new BufferedReader(getReaderWriterBuilder().getReader(new FileInputStream(targetFile)));
            String line = null;
            while((line = br.readLine()) != null)
                ps.println(line);
        }catch(Exception ex) {}
    }

    /**
     * Reloads the generated file, but change the value of the user blocks with the values
     * from the old file
     * @param userBlocks
     */
    protected void replaceUserBlocks(CodeSafetyMarksLoader userBlocks) throws IOException, FileNotFoundException {
        if (userBlocks.getMarks().size() == 0)
            return;

        String lineSeparator = AmCodeGenConsts.getObjectFactory().getInstance(ISystemProperties.class).getNewLine();
        List<String> codeLines = new Vector<String>();

        BufferedReader br = new BufferedReader(getReaderWriterBuilder().getReader(new FileInputStream(targetFile)));
        String line = null;
        while((line = br.readLine()) != null) {
            if (isLineBlockOpenTag(line)) {
                String blockName = getBlockName(line);

                /** adding open code block mark line */
                codeLines.add(line);
                
                /** Indicates if the code loader found the given user block */
                boolean validUserBlock = userBlocks.getMarks().containsKey(blockName);

                if (validUserBlock) {
                    /** copying contents */
                    for(String userLine : userBlocks.getMarks().get(blockName))
                        codeLines.add(userLine);
                }

                /** ignoring the contents until end the block */
                while((line = br.readLine()) != null) {

                    if (isLineBlockCloseTag(line)) {
                        /** adding close code block mark line */
                        codeLines.add(line);
                        break;
                    }
                    
                    if (!validUserBlock)
                        codeLines.add(line);
                }
            } else {
                codeLines.add(line);
            }
        }
        br.close();

        BufferedWriter bw = new BufferedWriter(getReaderWriterBuilder().getWriter(new FileOutputStream(targetFile)));
        for(String finalLine : codeLines) {
            bw.write(finalLine);
            bw.write(lineSeparator);
        }
        bw.flush();
        bw.close();
    }

    /**
     * Check if the target file is a valid file (not a directory)
     */
    protected void checkValidTargetFile() {
        if (getTargetFile().isDirectory())
            throw new IllegalStateException("The target file: " + getTargetFile().getAbsolutePath() + " cannot be a directory!");
    }

    /**
     * Make the backup of the original file
     */
    protected boolean makeBackupOfOriginal() {
        if (!getTargetFile().exists())
            return true;

        IFileBackupService fbs = AmCodeGenConsts.getObjectFactory().getInstance(IFileBackupService.class);

        return fbs.makeBackup(getTargetFile());
    }

    /**
     * Create and configure a new VelocityEngine object.
     * Configured to work with absolute template path
     * @return
     * @throws Exception
     */
    protected VelocityEngine configureVelocity() throws Exception {
        Properties props = new Properties();
        props.put("resource.loader", "file");
        props.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
        props.put("file.resource.loader.path", "");  // !!!
        props.put("file.resource.loader.cache", "true");

        VelocityEngine ve = new VelocityEngine(props);
        ve.init();
        return ve;
    }

    /**
     * Configure the default ObjectFactory instances to be used with the code generator
     */
    public static void configureDefaultFactories() {
        /** Register the global factory */
        ObjectFactory.initializeFactory(AmCodeGenConsts.getFactoryId());
        
        AmCodeGenConsts.getObjectFactory().register(ISystemProperties.class, new IObjectConstructor<ISystemProperties>() {

            public ISystemProperties createNew() {
                return SystemPropertiesImpl.getInstance();
            }

        });

        AmCodeGenConsts.getObjectFactory().register(IStreamReaderWriterBuilder.class, new IObjectConstructor<IStreamReaderWriterBuilder>() {

            public IStreamReaderWriterBuilder createNew() {
                return new Utf8ReaderWriterBuilder();
            }

        });

        AmCodeGenConsts.getObjectFactory().register(IFileBackupService.class, new IObjectConstructor<IFileBackupService>() {

            public IFileBackupService createNew() {
                return new FileBackupService();
            }
        });

        AmCodeGenConsts.getObjectFactory().register(IMethodExtender.class, new IObjectConstructor<IMethodExtender>(){

            public IMethodExtender createNew() {
                return new MethodExtenderImpl();
            }

        });
    }
}
